
本系列即將邁入後半段(現在才邁入後半段會不會有點晚?)—— 泛用型別(Generics)的介紹。
筆者翻閱很多資料發現,泛用型別儘管看似困難(這是要讓讀者不想學嗎?),但用途事實上真的很多,甚至會延伸到 ES6 的一些 Feature。
筆者在第三篇章的 UBike 地圖案例就有提到 —— ES6 Promise 與 Map 事實上都有用到泛用型別的註記(Type Annotation)表示行為。
以下大概是筆者設想可能會講到的東西:
同理,筆者在此發出一個聲明:第四篇章的內容順序不一定會按照上面的順序講解,筆者會按照適合的方式進行編排喔~~~
另外,如果是臨時看到這裡但剛學習 TypeScript 的讀者,除非你有 Java、C# 等靜態語言的學習背景,否則還是建議至少把第一篇章《前線維護》部分讀完,因為型別系統的推論與註記機制在本篇章討論的佔比又會回到很大又很變態的篇幅大小。
[2019.10.22 16:49 新增訊息]
由於最近處理的事情變多,連載速度會開始變慢,所以第四篇章還是未完成狀態 XD,但確定會超過 Day 50。
另外,本篇章由於圖的品質跟第三篇章有得拼,數量更是多了些,因此速度變慢也是正常的 QQ,請讀者給筆者時間整理一下。
貼心小提示
本系列內容預設編譯器設定
tsconfig.json裡的noImplictAny與strictNullChecks為啟動的狀態。如果對於這兩個選項有問題可以參見這一篇。
以下本篇章第一篇文 — 正文開始~
開篇就從最簡單看起來也很笨的的例子來舉。

Identity 這個型別化名的宣告裡,有一個被稱為型別參數(Type Parameter)的東西 —— 也就是 Identity<T> 裡面的 T 這個東西。
原理跟普通的函式概念差不多,只是可以把泛用型別看成是型別版的函式,代入的 Identity<T> 裡面的 T 為什麼樣的型別 —— T 就會成為那個型別。
比如說,Identity<number> 裡面的 T 被取代為 number 型別,將其註記到任何變數會得到如圖一的推論結果:

圖一:Identity<number> 中,把 T 取代為 number,則 Identity<number> 的結果就等於 number
基本上,泛用型別就是將型別化名進行參數化的動作,帶入不同的型別作為泛用型別的參數,就會得出不同的型別樣貌。
重點 1. 泛用型別的概念 Concept of Generic Types
泛用型別的意義最主要是將型別化名進行參數化(Parameterize)的動作,使得型別化名擁有更多的彈性與變化性。
筆者提到一個很重要的點:泛用型別就是將型別化名進行參數化的動作;也就是說,任何型別化名都可以轉換成泛用型別。
從筆者這樣的描述可以推論:型別(Types)、介面(Interface)與類別(Class)都可以轉換成泛用型式。
以下筆者就舉幾個例子:

Dictionary<T> 為一個泛用型別,代表的就是一般的鍵值對(Key-Value Pair)的物件格式。其中,如果 T 被代入為 boolean 型別,則如果值裡面含有非 boolean 型別的值就會被 TypeScript 警告。以下就舉幾個例子,展示給讀者看。(程式碼如下,檢測結果如圖二)


圖二:因為去旅遊韓國愉快的原因是 string 型別 —— 不符合 Dictionary<boolean> 指定的 boolean 型別,就會被 TypeScript 和諧掉(甘去韓國愉快旅遊 P 事)
至於泛用介面與泛用類別的使用情形~ 筆者把他們保留到後續篇章,因為這個討論串一開,筆者一定會不小心啪拉啪拉一連串把本篇擴張到沒完沒了到很難收尾的狀態(深感尷尬),筆者在本篇先把泛型的簡介內容都些丟出來給讀者做心理準備,後面講到一些泛型的應用對讀者來說也比較容易適應。
重點 2. 泛用型別化名 Generic Type Alias
泛用型別的表現形式不單單只有型別(Type)而已,介面與類別皆可以轉換成泛用型式。
在泛用型別化名的名稱後面接上的
<>內容就是型別參數(Type Parameter)的宣告。型別參數可以有複數個,只要在
<>裡用逗號分隔就可以了。
詳細的宣告過程細節、規則與程式碼公式化結果(筆者一貫的手法)會在後面的篇章慎重地呈現出來。
泛用的形式基本上是很全面地(Universal),連函式本身也可以變成泛用的形式,但是變化性可能對剛接觸泛型的讀者深感困惑。以下筆者把常見的函式型別的泛用型式刻意分成兩種。

首先,以上的案例宣告一個函式的型別化名 operator —— 這個 operator 有一個名為 T 的型別參數,而剛好 operator 裡面對應的函式型別的參數以及輸出也是 T 型別,就以 operator<number> 為範例的話,p1 與 p2 就會被限制為 number 型別,而輸出的型別也會是 number 型別。
所以以下的 addition 這個變數註記為 operator<number> 就代表 —— p1 與 p2 以及函式輸出必須為 number 型別;而 stringConcatenation 則是 operator<string> 就代表 —— p1 與 p2 以及函式的輸出必須為 string 型別。

事實上,以上的程式碼還有地方可以簡化,這部分就放在後續篇章討論不然會很難收尾 XD。

這裡並非是用泛用型別化名註記在變數上,然後再指派一個函式進去;以上的程式碼呈現的方式是 —— 你可以在宣告函式的時候就把它變成泛用的形式。
這個 identityFunc 有宣告一個型別參數 T,函式本身只有一個參數,對應型別是 T,而輸出的結果被註記為型別參數 T,也就是說:如果你呼叫該函式時是 identityFunc<string> 這樣呼叫的,輸入必須填數 string 型別,輸出也是 string 型別。
重點 3. 泛用函式表現行為 Generic Function Representation
若宣告的函式型別為泛用形式,泛用的型別參數除了可以註記到函式內部的變數外,還可以註記在函式的參數(Parameter)與輸出(Output)上。
理所當然,我們還可以使用多重的泛用參數,這其實在本篇的重點 2 的最後一句話有稍微提及,不過筆者暫且就展示給讀者看一下:

以上的 TypeConversion 型別有兩個型別參數,分別是 T 和 U。
以 isPositive 為例,它被註記到的 TypeConversion 它的 T 為 number 型別,U 則是 boolean 型別;而 TypeConversion 的輸入為 T 型別,輸出為 U 型別,isPositive 將輸入為 number 型別的東西,根據數學的正數判斷,轉換成 boolean 型別的值。
另外的 anythingToString 則是將 T 設定為 any 型別,U 則是 string,代表輸入可以為任何型別值,但輸出必須是 string,裡面的實作內容也僅僅是對輸入呼叫 toString 這個方法進行轉換。
事實上,泛用型別在 TypeScript 裡本來就有存在的痕跡,只是有沒有發覺到而已。
最簡單的就是陣列型別 —— Array<T> 這種型別 —— 它是內建的,等效於 T[]。
也就是說,用以下的方式表示也 OK:

以上的 MyArray<T> 是為了不要重複定義 Array<T> 而選擇用其他化名取代。實際上除了基本的 T[] 表示形式,也可以直接用 Array<T> 代表某陣列。

讀者可以試著檢測看看以上的程式碼,筆者就不貼出結果了。
其中,又以 ECMAScript 新增、偏向功能層面(Utility Aspect)的應用跟泛用型別的機制又息息相關,這又讓筆者不得不搬出 ES6 Promise、Map 與 Set 等東西與泛用型別的結合等應用,這些都會在後續篇章進行探討 —— 畢竟筆者寫作當下不希望讀者會了泛用型別,但是卻不曉得應用或者是哪些地方會看到存在 —— 事實上泛用型別處處存在,尤其對型別的推論(Type Inference)有非常大的影響。
沒有泛用型別的話,型別系統少了一半的推論能力也不足以為奇
感覺就是 —— 沒有泛用型別,這型別系統也就爛爛的 —— 所以本篇章也會剖析型別推論除了認讀者的型別註記(Type Annotation)以及根據語法的結構判斷型別推論(Syntatic Structure)外,泛用型別對於型別推論隱藏的巨大影響。
理解這裡面的機制事實上對於用 TypeScript 寫程式,除了會感到特別有趣外,還可以提升寫程式的效率。(如洪荒之力非常多!)
事實上,這也是從泛用型別延伸出來的變體,條件型別(Conditional Types)筆者很少看到有中文的文章在探討(不然就是筆者笨到連 Gxxgle 搜尋都不會用),英文討論條件型別的文章,恩... 不算少,但是也很少人正視這個東西,以為又是 TypeScript 額外延伸出來的功能。
但這並不完全是 TypeScript 的新功能,只能說算是部分新、但又是從泛用型別延伸出來的,因此筆者沒有把條件型別列入型別的一種表現形式的原因主要是因為 —— 它是衍伸物,並不太需要刻意立下一個新的型別種類。
以上廢話太多,不過筆者暫且舉一個還蠻不錯的條件型別案例,而且這本來就是 TypeScript 內建的,不過這些都被官方稱為 Utility Types,冠上的是這個 Utility 這個名稱,實質上是用條件型別的方式去實踐,但本質還是泛用型別的一種延伸出的表現行為。
官方真是麻煩,就好好講說是內建的條件型別也可以,偏偏再多冠上一個 Utility Types 名稱。
“好啦,趕快舉例!筆者不要發牢騷,廢話真多!”
Required 就是一種還蠻好用的條件型別。
在很久的之前探討過的選用屬性部分,只要被屬性旁標註為 ? 就代表不一定需要該屬性,可視為不存在。
另外,介面的宣告與註記行為,只要變數被註記到某介面就必須實作全部的功能,並且根據物件完整性的理論,該變數:
這應該對讀者來說已經是朗朗上口的原則(如果你完整看完本系列的話)。
但是 Required<T> 非常有趣,它的意思是 —— 它會將所有 T 裡面的選用屬性轉成必要的屬性。(圖三為檢測結果;圖四為錯誤訊息)


圖三:如果被冠上 Required 這個條件型別,內部的 PersonalInfo 介面會把所有的選用屬性轉成必要屬性

圖四:TypeScript 很明確地告訴你,Required<PersonalInfo> 的註記下,少了 hasPet 這個屬性
事實上,筆者這裡沒有講條件型別的寫法 —— 因為比泛用型別更複雜......很多 XDDDD。
所以筆者認為這個主題應該會放到本篇章很後面才會講到,但坦白說,讀者真的需要再讀也可以,除非你對於這東西感到興趣。
本篇開篇大致上介紹完讀者會看到的泛型大概會在哪裡出現~
泛型應用挺多的 —— 沒學過或聽過的讀者最好要有心理準備。XDDDDDDDD